Skip to content

fix(gax): propagate structured LRO error details to ApiException - #14022

Open
nnicolee wants to merge 8 commits into
mainfrom
feat/lro-generic-error-propagation
Open

fix(gax): propagate structured LRO error details to ApiException#14022
nnicolee wants to merge 8 commits into
mainfrom
feat/lro-generic-error-propagation

Conversation

@nnicolee

@nnicolee nnicolee commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Description:

When a Long-Running Operation (LRO) completes with a failure, structured error details inside the operation's error payload were previously dropped. This made it impossible for client applications to access fine-grained provider error messages (such as quota or usage violations) via the public ApiException.getErrorDetails() API.

This PR implements Phase 1 of the LRO error propagation design by establishing the transport-agnostic mechanism to propagate structured LRO error details in GAX for both gRPC and HTTP/JSON (REST) transports.

Design doc: go/sdk:java-lro-error-details

Key Changes:

  1. Core GAX Interfaces:
    • Added support to fetch structured error details from the LRO operation snapshots.
  2. gRPC Transport:
    • Implemented error details extraction from LRO operation error payloads (only populated if structured error details are present).
    • Updated the gRPC response transformer to pass these error details when converting failed operations into exception objects.
    • Added unit test validation checking details propagation for LRO failures over gRPC.
  3. HTTP/JSON (REST) Transport:
    • Implemented error details extraction and storage in REST LRO operation snapshots (only populated if structured error details are present).
    • Updated the REST response transformer to fetch and pass the snapshot's error details.
    • Added unit test validation checking details propagation for LRO failures over HTTP/JSON.
  4. Integration & Unit Testing:
    • Added a Showcase integration test verifying successful end-to-end propagation of error details over gRPC.
    • Note on HTTP/JSON Integration Test: Deferring the end-to-end Showcase integration test for HTTP/JSON LROs to Phase 2 because HTTP/JSON relies on a TypeRegistry in generated stubs to parse custom/standard payload types packed in Any details, the test client fails to deserialize these details. Once we roll out the generator changes in Phase 2 to automatically add error details types to generated stub registries, the Showcase REST integration test will pass out-of-the-box. We have added unit tests to cover the HTTP/JSON LRO response parsing pathway in the interim.

Testing:

  • Verified that all gRPC and HTTP/JSON transformer unit tests pass successfully:
    mvn test -pl sdk-platform-java/gax-java/gax-grpc,sdk-platform-java/gax-java/gax-httpjson -Dtest=ProtoOperationTransformersTest
  • Verified that the Showcase LRO integration test passes successfully:
    mvn test -pl java-showcase/gapic-showcase -Dtest=ITLongRunningOperation

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for propagating error details in long-running operations (LRO) for both gRPC and HTTP/JSON transports. This is achieved by adding getErrorDetails() to the OperationSnapshot interface and implementing it in GrpcOperationSnapshot and HttpJsonOperationSnapshot. The ProtoOperationTransformers are updated to pass these error details when throwing exceptions, and corresponding integration and unit tests are added. Feedback on the changes includes renaming a misleadingly named test that asserts error propagation rather than dropping, and ensuring that ErrorDetails is only instantiated and returned in GrpcOperationSnapshot and HttpJsonOperationSnapshot if there are actual details present (i.e., checking that the details count is greater than zero).

@nnicolee

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for propagating error details from long-running operations (LROs) across both gRPC and HTTP/JSON transports by introducing a getErrorDetails() method to the OperationSnapshot interface and implementing it in GrpcOperationSnapshot and HttpJsonOperationSnapshot. It also updates ProtoOperationTransformers to pass these error details when throwing exceptions and adds corresponding integration and unit tests. Feedback on the changes highlights a potential issue in HttpJsonOperationSnapshot.Builder.setOperation where errorDetails is not reset to null if the operation has no error, which could lead to stale state if the builder is reused.

@nnicolee

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for propagating error details in long-running operations (LRO) for both gRPC and HTTP/JSON transports. It introduces the getErrorDetails() method to the OperationSnapshot interface and implements it in GrpcOperationSnapshot and HttpJsonOperationSnapshot. Additionally, it updates the ProtoOperationTransformers to include these error details when constructing exceptions, and adds corresponding unit and integration tests. The reviewer suggested adding a timeout to the operationFuture.get() call in the new integration test to prevent the test suite from hanging indefinitely.

@nnicolee
nnicolee marked this pull request as ready for review August 10, 2026 14:21
@nnicolee
nnicolee requested review from a team as code owners August 10, 2026 14:21
@nnicolee
nnicolee requested a review from lqiu96 August 10, 2026 14:21
Comment on lines +79 to +81
default @Nullable ErrorDetails getErrorDetails() {
return null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq, what do you think about returning a non-null empty ErrorDetails instead of null here? I think this would be similar to the proto messages where there is a default value.

IIUC, this is primarily used within ProtoOperationTransformers's ResponseTransformer which operations on a response and it should return Status.getDefaultInstance() if there is no error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great suggestion! this would aligns more with the proto messages! I have updated the interface to return an empty ErrorDetails rather than null!

* @param errorDetails the LRO error details
* @return the builder instance
*/
public Builder setErrorDetails(final @Nullable ErrorDetails errorDetails) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is only for testing right? If so, can we make this package-private and enforce that ErrorDetails cannot be nullable?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other question with this: I don't think I see a gRPC variant for this. Do we need to expose this setter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this is only used for unit testing inside com.google.api.gax.httpjson. I have updated setErrorDetails to be package-private and non-nullable. The gRPC snapshot class (GrpcOperationSnapshot) is package-private and doesn't use a builder (it is constructed via the static create(Operation) factory), which is why it doesn't have a setter!

Comment on lines +86 to +91
if (operation.hasError() && operation.getError().getDetailsCount() > 0) {
return ErrorDetails.builder()
.setRawErrorMessages(operation.getError().getDetailsList())
.build();
}
return ErrorDetails.builder().setRawErrorMessages(Collections.emptyList()).build();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the if block here? Can we just do something like

ErrorDetails.builder().setRawErrorMessages(operation.getError().getDetailsList()).build();?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes! since it returns an empty list by default, we can construct this directly, updated!

@@ -193,4 +199,35 @@ void testHttpJson_LROUnsuccessfulResponse_exceedsTotalTimeout_throwsDeadlineExce
TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
}
}

@Test
void testGRPC_LROErrorResponse_propagatesErrorDetails() throws Exception {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know parsing HttpJson is a bit more involved/ difficult since we need to manually unpack the Any proto. Would it be possible to also add a HttpJson variant as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added testHttpJson_LROErrorResponse_propagatesErrorDetails!

to support this, I made ProtoRestSerializer.create(TypeRegistry) public in gax-httpjson so that stubs can serialize custom Any fields in requests, and registered PoetryError in HttpJsonEchoStub's static type registry so the serialization of the wait request body succeeds!

Comment on lines +204 to +214
void testGRPC_LROErrorResponse_propagatesErrorDetails() throws Exception {
EchoClient grpcClient = TestClientInitializer.createGrpcEchoClient();
try {
PoetryError poetryError =
PoetryError.newBuilder().setPoem("Roses are red, violets are blue").build();
Status status =
Status.newBuilder()
.setCode(Code.ALREADY_EXISTS_VALUE)
.setMessage("The resource already exists")
.addDetails(Any.pack(poetryError))
.build();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I know we're blocking on the ErrorDetails Showcase test. I think these tests may be better suited in that file. Let's have this live here for now and then we can move it over there in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, will move them later once the PRs become unblocked!

@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed for 'gapic-generator-java-root'

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants